package com.c0der.neat;

import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.annotation.JSONField;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Genome implements Serializable {
    private static final int HIDDEN_NODES = 100000000;
    private static final Random rand = Config.rand;

    private int inputs, outputs;
    private List<Connection> connections = new ArrayList<>();

    private double fitness;

    @JSONField(serialize = false)
    private Map<Integer, Node> nodes = new HashMap<>();

    @JSONField(serialize = false)
    private Config config;

    public Genome() {

    }

    public Genome(Config config) {
        this.inputs = config.inputs;
        this.outputs = config.outputs;
        this.config = config;
    }

    public Genome(Config config, List<Connection> connections) {
        this(config);
        for (Connection connection : connections)
            this.connections.add(new Connection(connection));
        generateNetwork();
    }

    public Map<Integer, Node> getNodes() {
        return nodes;
    }

    public List<Connection> getConnections() {
        return connections;
    }

    public Genome(Genome genome) {
        this(genome.config, genome.connections);
    }

    public double getFitness() {
        return fitness;
    }

    public void setFitness(double fitness) {
        this.fitness = fitness;
    }

    private int getInputsId(int i) {
        return i;
    }

    private int getOutputsId(int i) {
        return inputs + HIDDEN_NODES + i;
    }

    private int getHiddenId(int i) {
        return inputs + i;
    }

    public void generateNetwork() {
        nodes.clear();
        for (int i = 0; i < inputs; i++)
            nodes.put(getInputsId(i), new Node());
        for (int i = 0; i < outputs; i++)
            nodes.put(getOutputsId(i), new Node());
        for (Connection connection : connections) {
            if (!nodes.containsKey(connection.getFrom()))
                nodes.put(connection.getFrom(), new Node());
            if (!nodes.containsKey(connection.getTo()))
                nodes.put(connection.getTo(), new Node());
            nodes.get(connection.getTo()).addFrom(connection);
        }
    }

    int dd = 0;

    public double[] evaluate(double[] inputs) {
        if (this.inputs != inputs.length)
            return null;
        for (Node node : nodes.values())
            node.setValue(0);
        double[] r = new double[outputs];
        for (int i = 0; i < this.inputs; i++)
            nodes.get(getInputsId(i)).setValue(inputs[i]);
        for (int i = 0; i < outputs; i++) {
            dd = 0;
            r[i] = nodes.get(getOutputsId(i)).getValue(nodes);
//                    evaluate();
        }
        return r;
    }

//    private double evaluate(Node node) {
//        dd++;
//        if (dd == 20)
//            System.out.println(toString());
//        List<Connection> connections = node.getFrom();
//        double value = 0;
//        for (Connection connection : connections) {
//            if (!connection.isEnable())
//                continue;
//            Node _node = nodes.get(connection.getFrom());
//            double v = _node.getValue(nodes);
//            if (v == null) {
//                v = evaluate(_node);
//                _node.setValue(v.doubleValue());
//            }
//            value += v.doubleValue() * connection.getWeight() + connection.getBias();
//        }
//        value = ActiveFunction.Sigmod.function.active(value);
//        return value;
//    }

    public synchronized void mutate() {
        generateNetwork();
        mutateNewNode();
        mutateNewConnection();
        mutateConnections();
        generateNetwork();
    }

    private void mutateNewConnection() {
        if (!config.newConnection())
            return;
        generateNetwork();
        int from = rand.nextInt(nodes.size());
        int to = rand.nextInt(nodes.size() - inputs) + inputs;
        int node1 = -1;
        int node2 = -1;

        int i = 0, j = 0;
        for (int k : nodes.keySet()) {
            if (from == i) {
                node1 = k;
                break;
            }
            i++;
        }

        for (int k : nodes.keySet()) {
            if (to == j) {
                node2 = k;
                break;
            }
            j++;
        }
        if (node1 >= node2)
            return;
        Node node = nodes.get(node1);
        for (Connection connection : node.getFrom()) {
            if (connection.getFrom() == node2)
                return;
        }
        node = nodes.get(node2);
        for (Connection connection : node.getFrom()) {
            if (connection.getFrom() == node1)
                return;
        }
        Connection connection = new Connection(node1, node2, config.weight(0), config.bias(0), true);
        if (!connections.contains(connection))
            connections.add(connection);
    }

    private void mutateNewNode() {
        if (!config.newNode())
            return;
        generateNetwork();
        if (connections.size() > 0) {
            Connection randomCon = connections.get(rand.nextInt(connections.size()));
            if (!randomCon.isEnable())
                return;
            int nextNode = nodes.size() - outputs;
            randomCon.setEnable(true);
            connections.add(new Connection(randomCon.getFrom(), nextNode, config.weight(0), config.bias(0), true));        // Add innovation and weight
            connections.add(new Connection(nextNode, randomCon.getTo(), randomCon.getWeight(), randomCon.getBias(), true));
        }
    }

    private void mutateConnections() {
        for (Connection connection : connections) {
            connection.setWeight(config.weight(connection.getWeight()));
            connection.setEnable(config.able(connection.isEnable()));
            connection.setBias(config.weight(connection.getBias()));
        }
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Genome genome = (Genome) o;
        return inputs == genome.inputs &&
                outputs == genome.outputs;
    }

    @Override
    public int hashCode() {
        return Objects.hash(inputs, outputs);
    }

    @Override
    public String toString() {
        return JSONObject.toJSONString(this);
    }

    public void writeToFile(File file) {
        String json = toString();
        BufferedOutputStream bos = null;
        FileOutputStream fos = null;
        try {
            byte[] data = Zip.zip(json.getBytes());
            fos = new FileOutputStream(file);
            bos = new BufferedOutputStream(fos);
            bos.write(data);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (bos != null) {
                try {
                    bos.close();
                } catch (IOException e1) {
                    e1.printStackTrace();
                }
            }
            if (fos != null) {
                try {
                    fos.close();
                } catch (IOException e1) {
                    e1.printStackTrace();
                }
            }
        }
    }

    public static Genome readFromFile(File file) {
        BufferedInputStream bis = null;
        FileInputStream fis = null;
        try {
            fis = new FileInputStream(file);
            bis = new BufferedInputStream(fis);
            byte[] data = new byte[bis.available()];
            bis.read(data);
            String json = new String(Zip.unZip(data));
            return JSONObject.parseObject(json, Genome.class);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (bis != null) {
                try {
                    bis.close();
                } catch (IOException e1) {
                    e1.printStackTrace();
                }
            }
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException e1) {
                    e1.printStackTrace();
                }
            }
        }
        return null;
    }


    public static Genome x(Genome g1, Genome g2) {
        if (!g1.equals(g2))
            return null;
//        double w;
//        if (g1.getFitness() > g2.getFitness())
//            w = 0.5d + (g1.getFitness() - g2.getFitness()) / g2.getFitness();
//        else if (g1.getFitness() < g2.getFitness())
//            w = 0.5d + (g1.getFitness() - g2.getFitness()) / g1.getFitness();
//        else
//            w = 0.5d;
        Map<Integer, Connection> geneMap1 = new HashMap<>();
        Map<Integer, Connection> geneMap2 = new HashMap<>();

        for (Connection con : g1.connections) {
            geneMap1.put(con.getInnovation(), con);
        }

        for (Connection con : g1.connections) {
            geneMap2.put(con.getInnovation(), con);
        }

        Set<Integer> innovationP1 = geneMap1.keySet();
        Set<Integer> innovationP2 = geneMap2.keySet();

        Set<Integer> allInnovations = new HashSet<Integer>(innovationP1);
        allInnovations.addAll(innovationP2);

        List<Connection> connections = new ArrayList<>();

        for (int key : allInnovations) {
            Connection trait;

            if (geneMap1.containsKey(key) && geneMap2.containsKey(key)) {
                trait = rand.nextBoolean() ? geneMap1.get(key) : geneMap2.get(key);
            } else {
                trait = geneMap1.containsKey(key) ? geneMap1.get(key) : geneMap2.get(key);
            }

//            if (rand.nextBoolean())
//                continue;
            connections.add(trait);
        }
        return new Genome(g1.config, connections);
    }

    private static final int S = 8;
    private static final int SX = 50;
    private static ExecutorService executorService = Executors.newFixedThreadPool(S);

    public interface CC {
        double cc(Genome genome);
    }

    public static List<Genome> calculateFitness(List<Genome> genomes, CC cc) {
        for (Genome genome : genomes) {
            genome.setFitness(cc.cc(genome));
        }
        return genomes;
    }

    public static List<Genome> newGeneration(int size, final Genome... parents) {
        final List<Genome> generations = Collections.synchronizedList(new ArrayList<Genome>());
        generations.addAll(Arrays.asList(parents));
        for (int i = 0; i < size; i++) {
            Genome genome1 = parents[rand.nextInt(parents.length)];
            Genome genome2 = parents[rand.nextInt(parents.length)];
            Genome genome = Genome.x(genome1, genome2);
            genome.mutate();
            generations.add(genome);
        }
        return generations;
    }
}
